const config = require('./config.js');
const http = require('http');
const chalk = require('chalk');
const path = require('path');
const url = require('url');
const {inspect,promisify} = require('util');
const fs = require('fs');
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const mime = require('mime');
const zlib = require('zlib');
const handlebars = require('handlebars');
//该方法执行后会返回得到一个渲染的方法，传入实际数据就可以得到渲染后的HTML
function list(){ //因为只在系统启动使用一次，使用同步异步无所谓
  const tmpl = fs.readFileSync(path.resolve(__dirname,'template','list.html'),'utf8');
  return handlebars.compile(tmpl);
}

/* //如果使用ejs打开这个
const ejs = require('ejs'); // 模板引擎
const template = readFileSync(path.resolve(__dirname,'template.html'),'utf8');*/

const debug = require('debug')('static:app');
//这是一个在控制台输出的模块，引入时返回一个函数
//这个函数执行后又会返回一个函数
//是否在控制台打印取决于【环境变量中DEBUG的值】是否等于static:app
//设置的环境变量有特点，约定第一部分是项目名，第二部分是模块名
//set DEBUG=static:app
//export DEBUG=static:app

class Server {
  constructor(argv){
    this.list = list();
    this.config = Object.assign({},config,argv);
    // this.template = template; //如果使用ejs打开这个
  }
  start(){
    const server = http.createServer();
    server.on('request',this.request.bind(this));
    server.listen(this.config.port,()=>{
      const url = `${this.config.host}:${this.config.port}`;
      debug(`server started at ${chalk.green(url)}`);
    });
  }

  async request(req,res){
    //先取到客户端向访问的文件或文件夹路径
    let{pathname} = url.parse(req.url);
    pathname = decodeURIComponent(pathname); //默认中文在url栏中发送之前会被浏览器转译
    if(pathname === '/favicon.ico') return res.end(); //网站小图标 这个请求是随机发送的(?)

    const filepath = path.join(this.config.root,pathname);
    console.log('req.url:',req.url);

    try{
      const statObj = await stat(filepath);
      if(statObj.isDirectory()){ //如果是目录，应该显示目录下的文件列表
        let files = await readdir(filepath); //返回的是文件名组成的数组（不带路径）
        files = files.map(file=>({
          name:file,
          url: path.join(pathname, file)
        }));

        /* //如果使用ejs打开这个
        const html = ejs.render(this.template,{
          arr:dirs,
          // 如果路径是/ 那就不需要增加路径
          currentPath:pathname === '/'?'':pathname
        });*/
        const html = this.list({
          title:pathname,
          files
        });
        res.setHeader('Content-Type','text/html;charset=utf-8');
        res.end(html);
      }else{
        this.sendFile(req,res,filepath,statObj);
      }

    }catch(e){
      debug(inspect(e)); //inspect把一个toString后的对象仍然能展开显示
      /*
        const {inspect} = require('util');
        console.log({a:1}.toString()) //[object Object]
        console.log(inspect({a:1}).toString()) //{a:1}
      */
      this.sendError(req,res);
    }
  }

  sendFile(req,res,filepath,statObj){
    if (this.handleCache(req, res, filepath, statObj)) { //如果走缓存，则直接返回
      res.writeHead(304); //res.statusCode = 304
      return res.end();
    }
    res.setHeader('Content-type',mime.getType(filepath)+';charset=utf-8');
    const encoding = this.getCompressDuplexStream(req,res); // 同crypto出来的一样，是个转化流
    const rs = this.getRangeStream(req,res,filepath,statObj);
    if(encoding){
      rs.pipe(encoding).pipe(res); //先交给转换流（encoding）处理，再分发给res
    }else{
      rs.pipe(res);
    }
  }
  sendError(req,res){
    res.statusCode = 500;
    res.end(`there is something wrong in the server! please try later!`);
  }
  handleCache(req,res,filepath,statObj){
    const ifModifiedSince = req.headers['if-modified-since'];
    const isNoneMatch = req.headers['is-none-match'];
    res.setHeader('Cache-Control','private,max-age=30');
    res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toGMTString()); //这个expires的强制缓存的时间格式不能随便写
    const etag = statObj.size; //new Date(statObj.ctime.toGMTString()).getTime()+'-'+statObj.size
    const lastModified = statObj.ctime.toGMTString();
    res.setHeader('ETag',etag);
    res.setHeader('Last-Modified',lastModified);

    return !(
      !(isNoneMatch || ifModifiedSince)/*有可能是第一次请求 从来没设置过缓存相关的头信息*/ ||
      isNoneMatch && isNoneMatch !== etag ||
      ifModifiedSince && ifModifiedSince !== lastModified
    );

    //为true则需要更新，否则就走缓存
  }
  getRangeStream(req,res,filepath,statObj){
    let start = 0;
    let end = statObj.size - 1;
    const range = req.headers['range'];
    if(range){
      let result = range.match(/bytes=(\d*)-(\d*)/); //不可能有小数，网络传输的最小单位为一个字节
      if(result){
        start = isNaN(result[1])?0:parseInt(result[1]);
        end = isNaN(result[2])?end:parseInt(result[2]);
      }
      res.setHeader('Accept-Range','bytes');
      res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`)
      res.statusCode = 206; //Partial Content  返回整个数据的一块
    }
    return fs.createReadStream(filepath,{
      start:start,end:end
    });
  }
  getCompressDuplexStream(req,res){
    const acceptEncoding = req.headers['accept-encoding'];
    if (/\bgzip\b/.test(acceptEncoding)) {
      /*
        \b是正则表达式规定的一个特殊代码，代表着单词的开头或结尾，也就是单词的分界处。虽然通常英文的单词是由空格，标点符号或者换行来分隔的，但是\b并不匹配这些单词分隔字符中的任何一个，它只匹配一个位置。
        var text = ".abc";
        var reg1 = /\b\.abc\b/;
        var reg2 = /^\.abc$/;
        console.log( reg1.test(text) ); //false
        console.log( reg2.test(text) );//true
      */
      res.setHeader('Content-Encoding', 'gzip');
      return zlib.createGzip();
    } else if (/\bdeflate\b/.test(acceptEncoding)) {
      res.setHeader('Content-Encoding', 'deflate');
      return zlib.createDeflate();
    } else {
      return null;
    }
  }
}

module.exports = Server;
